Перейти к основному содержимому

Простые приложения на Go

Разработчику Архитектору

Простые приложения на Go

Язык программирования Go (Golang) создан для написания надежных, эффективных и понятных системных утилит. Его стандартная библиотека предоставляет мощные инструменты для работы с файлами, сетью, параллелизмом и структурированными данными без необходимости подключения сторонних зависимостей. В этой главе рассматриваются практические примеры создания консольных приложений и серверов, демонстрирующие ключевые особенности синтаксиса и экосистемы языка.

Генератор паролей

Генератор паролей демонстрирует работу со строками, массивами символов и генерацией случайных чисел. Стандартная библиотека math/rand позволяет создавать криптографически стойкие последовательности через пакет crypto/rand.

Код примера

package main

import (
"crypto/rand"
"fmt"
"math/big"
)

func generatePassword(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
if length <= 0 {
return ""
}

password := make([]byte, length)
for i := range password {
// Получаем случайное число в диапазоне [0, len(charset))
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
panic(err)
}
password[i] = charset[num.Int64()]
}

return string(password)
}

func main() {
pass := generatePassword(16)
fmt.Println("Сгенерированный пароль:", pass)
}

Разбор кода

  • Пакет crypto/rand: Используется вместо math/rand для получения безопасных случайных чисел, что критично для генерации паролей.
  • Структура данных []byte: Слайс байтов используется как буфер для хранения символов будущего пароля. Это эффективно по памяти.
  • Цикл for range: Итерация по слайсу автоматически определяет индекс и значение элемента.
  • Преобразование типов: Функция num.Int64() преобразует большое целое число из big.Int в стандартный тип int64 для использования в качестве индекса строки.

Сортировщик текстового файла

Этот пример показывает чтение текста из файла, разделение его на строки, сортировку списка строк и запись результата обратно в файл.

Код примера

package main

import (
"bufio"
"fmt"
"os"
"sort"
"strings"
)

func sortFile(inputPath, outputPath string) error {
file, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("ошибка открытия файла: %w", err)
}
defer file.Close()

var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" {
lines = append(lines, line)
}
}

if err := scanner.Err(); err != nil {
return fmt.Errorf("ошибка чтения: %w", err)
}

sort.Strings(lines)

outFile, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("ошибка создания файла: %w", err)
}
defer outFile.Close()

writer := bufio.NewWriter(outFile)
for _, line := range lines {
if _, err := writer.WriteString(line + "\n"); err != nil {
return fmt.Errorf("ошибка записи: %w", err)
}
}

return writer.Flush()
}

func main() {
err := sortFile("input.txt", "output.txt")
if err != nil {
fmt.Println("Ошибка:", err)
os.Exit(1)
}
fmt.Println("Файл успешно отсортирован.")
}

Разбор кода

  • bufio.Scanner: Эффективный инструмент для посимвольного или построчного чтения больших файлов без загрузки всего содержимого в память.
  • strings.TrimSpace: Удаляет пробелы и символы перевода строки по краям каждой строки перед обработкой.
  • sort.Strings: Встроенная функция сортировки, которая использует алгоритм быстрой сортировки (quicksort) для упорядочивания слайса строк.
  • defer: Обеспечивает закрытие файловых дескрипторов при выходе из функции, предотвращая утечки ресурсов.
  • writer.Flush(): Явно сбрасывает буфер вывода на диск, гарантируя сохранение данных.

Консольный калькулятор

Калькулятор реализует базовую арифметику с обработкой ввода пользователя и разделением логики вычислений на отдельные функции.

Код примера

package main

import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)

func calculate(a float64, b float64, op string) (float64, error) {
switch op {
case "+":
return a + b, nil
case "-":
return a - b, nil
case "*":
return a * b, nil
case "/":
if b == 0 {
return 0, fmt.Errorf("деление на ноль невозможно")
}
return a / b, nil
default:
return 0, fmt.Errorf("неподдерживаемая операция: %s", op)
}
}

func main() {
reader := bufio.NewReader(os.Stdin)

fmt.Print("Введите первое число: ")
num1Str, _ := reader.ReadString('\n')
num1, _ := strconv.ParseFloat(strings.TrimSpace(num1Str), 64)

fmt.Print("Введите операцию (+, -, *, /): ")
opStr, _ := reader.ReadString('\n')
op := strings.TrimSpace(opStr)

fmt.Print("Введите второе число: ")
num2Str, _ := reader.ReadString('\n')
num2, _ := strconv.ParseFloat(strings.TrimSpace(num2Str), 64)

result, err := calculate(num1, num2, op)
if err != nil {
fmt.Println("Ошибка:", err)
return
}

fmt.Printf("Результат: %.2f\n", result)
}

Разбор кода

  • Тип float64: Используется для представления вещественных чисел с высокой точностью.
  • switch: Конструкция выбора действия, которая четко разделяет логику для каждого оператора.
  • Обработка ошибок: Возврат ошибки при попытке деления на ноль или вводе некорректной операции.
  • strconv.ParseFloat: Преобразование строкового ввода в числовой тип с проверкой на корректность формата.

Трекер задач в JSON

Пример демонстрирует сериализацию (преобразование структур данных в JSON) и десериализацию, а также работу с файловой системой для сохранения состояния.

Структура данных

type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
Priority int `json:"priority"` // 1-высокий, 5-низкий
}

type TaskList struct {
Tasks []Task `json:"tasks"`
}

Код реализации

package main

import (
"encoding/json"
"fmt"
"os"
)

const dataFile = "tasks.json"

func loadTasks() (*TaskList, error) {
data, err := os.ReadFile(dataFile)
if err != nil {
if os.IsNotExist(err) {
return &TaskList{Tasks: []Task{}}, nil
}
return nil, err
}

var list TaskList
if err := json.Unmarshal(data, &list); err != nil {
return nil, err
}
return &list, nil
}

func saveTasks(list *TaskList) error {
data, err := json.MarshalIndent(list, "", " ")
if err != nil {
return err
}
return os.WriteFile(dataFile, data, 0644)
}

func addTask(list *TaskList, title string, priority int) {
newID := 1
if len(list.Tasks) > 0 {
newID = list.Tasks[len(list.Tasks)-1].ID + 1
}
list.Tasks = append(list.Tasks, Task{
ID: newID,
Title: title,
Done: false,
Priority: priority,
})
}

func main() {
list, err := loadTasks()
if err != nil {
fmt.Println("Ошибка загрузки:", err)
os.Exit(1)
}

addTask(list, "Изучить Go", 1)
addTask(list, "Написать статью", 2)

if err := saveTasks(list); err != nil {
fmt.Println("Ошибка сохранения:", err)
os.Exit(1)
}

fmt.Println("Задачи сохранены в tasks.json")

// Вывод для проверки
jsonOut, _ := json.MarshalIndent(list, "", " ")
fmt.Println(string(jsonOut))
}

Разбор кода

  • Теги json:"...": Позволяют управлять именами полей в JSON-структуре. Например, поле Done сохраняется как done.
  • encoding/json: Стандартный пакет для работы с форматом JSON.
  • json.MarshalIndent: Преобразует структуру в строку JSON с отступами для удобного чтения человеком.
  • os.WriteFile: Современный аналог создания и записи файла с указанием прав доступа (0644).
  • Логика ID: Простая эвристика для генерации уникального идентификатора на основе последнего элемента списка.

Простой HTTP-сервер и клиент

Go обладает встроенным веб-сервером, который работает очень быстро благодаря встроенной поддержке многопоточности.

Серверная часть

package main

import (
"fmt"
"log"
"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "Гость"
}
fmt.Fprintf(w, "Привет, %s! Добро пожаловать на сервер Go.", name)
}

func main() {
http.HandleFunc("/", handler)
fmt.Println("Сервер запущен на http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}

Клиентская часть

package main

import (
"fmt"
"io"
"net/http"
)

func main() {
resp, err := http.Get("http://localhost:8080/?name=Разработчик")
if err != nil {
fmt.Println("Ошибка запроса:", err)
return
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Ошибка чтения тела:", err)
return
}

fmt.Println("Ответ сервера:")
fmt.Println(string(body))
}

Разбор кода

  • http.HandleFunc: Регистрирует функцию обработки запросов для конкретного URL-пути.
  • r.URL.Query().Get("name"): Извлекает параметры из строки запроса (например, ?name=Value).
  • http.Response: Объект ответа содержит статус код, заголовки и тело ответа.
  • io.ReadAll: Чтение всего тела ответа в байтовый срез.
  • defer resp.Body.Close(): Обязательная практика для освобождения сетевого ресурса после завершения работы.

Отправитель HTTP-запросов (с кастомными параметрами)

Этот пример расширяет функциональность клиента, позволяя отправлять POST-запросы с произвольными данными и заголовками.

Код примера

package main

import (
"bytes"
"fmt"
"io"
"net/http"
)

func sendPostRequest(url string, payload map[string]string) error {
jsonData := []byte(`{"key": "value"}`) // Упрощенный пример JSON
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return err
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Go-Custom-Client/1.0")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
fmt.Printf("Статус: %d, Тело: %s\n", resp.StatusCode, string(body))
return nil
}

func main() {
sendPostRequest("http://example.com/api/data", nil)
}

Разбор кода

  • http.NewRequest: Создает объект запроса с указанным методом (GET, POST, PUT и т.д.) и телом.
  • bytes.NewBuffer: Позволяет использовать байтовый срез как поток данных для тела запроса.
  • req.Header.Set: Установка пользовательских заголовков, важных для идентификации клиента или формата данных.
  • client.Do: Выполняет запрос и возвращает ответ, позволяя настроить таймауты и редиректы через структуру http.Client.

Утилита для сканирования директорий

Инструмент для обхода файловой системы и сбора информации о файлах и подпапках.

Код примера

package main

import (
"fmt"
"os"
"path/filepath"
)

func scanDirectory(root string) error {
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

indent := ""
relPath, _ := filepath.Rel(root, path)
for i := 0; i < len(filepath.SplitList(relPath)); i++ {
indent += " "
}

typeStr := "файл"
if info.IsDir() {
typeStr = "каталог"
}

fmt.Printf("%s[%s] %s (%d байт)\n", indent, typeStr, info.Name(), info.Size())
return nil
})
}

func main() {
currentDir, _ := os.Getwd()
fmt.Println("Сканирование директории:", currentDir)
if err := scanDirectory(currentDir); err != nil {
fmt.Println("Ошибка:", err)
}
}

Разбор кода

  • filepath.Walk: Рекурсивно проходит по всей структуре директории, вызывая функцию для каждого найденного пути.
  • os.FileInfo: Структура, содержащая метаданные файла (размер, права доступа, время изменения).
  • info.IsDir(): Метод для определения типа объекта (файл или каталог).
  • filepath.Rel: Вычисление относительного пути для красивого отображения структуры.

Скрипт для создания резервного копирования файлов

Автоматизация процесса дублирования файлов с добавлением временной метки к имени копии.

Код примера

package main

import (
"fmt"
"io"
"os"
"path/filepath"
"time"
)

func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()

destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()

if _, err := io.Copy(destFile, sourceFile); err != nil {
return err
}
return nil
}

func createBackup(sourceDir string) error {
timestamp := time.Now().Format("2006-01-02_15-04-05")
backupName := "backup_" + timestamp
backupPath := filepath.Join(sourceDir, backupName)

files, err := os.ReadDir(sourceDir)
if err != nil {
return err
}

for _, file := range files {
if file.IsDir() {
continue
}

srcPath := filepath.Join(sourceDir, file.Name())
dstPath := filepath.Join(backupPath, file.Name())

// Создание целевой директории бэкапа
os.MkdirAll(backupPath, 0755)

if err := copyFile(srcPath, dstPath); err != nil {
fmt.Printf("Не удалось скопировать %s: %v\n", file.Name(), err)
continue
}
fmt.Printf("Скопировано: %s\n", file.Name())
}
return nil
}

func main() {
dir := "."
fmt.Println("Начало резервного копирования...")
if err := createBackup(dir); err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Println("Резервное копирование завершено.")
}
}

Разбор кода

  • time.Now().Format: Форматирование даты в строку, пригодную для имени файла (ISO 8601 стиль адаптирован под Windows/Linux).
  • io.Copy: Оптимизированная функция для побайтового копирования данных между потоками.
  • os.MkdirAll: Создание директории, если она не существует, включая все необходимые промежуточные уровни.
  • filepath.Join: Корректное объединение путей с учетом разделителей для разных операционных систем.

Мониторинг дискового пространства

Утилита проверяет свободное место на диске и выводит статистику в процентах и абсолютных значениях.

Код примера

package main

import (
"fmt"
"os"
)

func checkDiskUsage(path string) error {
fs, err := os.Stat(path)
if err != nil {
return err
}

// Для Unix-систем используем syscall, но в чистом Go лучше использовать пакеты уровня выше
// Здесь пример с использованием статистики корневого каталога
// Примечание: Для точного объема диска часто требуется syscall или внешние библиотеки
// Но мы можем получить объем доступной памяти через os.Stat для тестов

// Альтернативный подход: получение статистики корня
rootStat, err := os.Stat("/")
if err != nil {
// Если нет прав на корень, пробуем текущую директорию
rootStat, err = os.Stat(".")
if err != nil {
return err
}
}

// В Go нет встроенной функции для получения общего объема диска в stdlib
// Поэтому этот пример демонстрирует концепцию проверки свободного места
// через анализ доступных операций или использование внешних инструментов.
// Однако, для демонстрации логики:

fmt.Printf("Путь: %s\n", path)
fmt.Printf("Статус: Доступен\n")

// Пример вывода (для реального проекта используйте syscall.Unix or golang.org/x/sys/unix)
// var stat syscall.Statfs_t
// syscall.Statfs(path, &stat)
// total := uint64(stat.Blocks) * uint64(stat.Bsize)
// free := uint64(stat.Bfree) * uint64(stat.Bsize)

fmt.Println("Метод: Анализ метаданных директории (демонстрация)")
return nil
}

func main() {
checkDiskUsage("/tmp") // Или текущая директория
}

Примечание: Стандартная библиотека Go не предоставляет прямой функции для получения общего объема диска и свободного места. Для этого обычно используют пакет syscall (Unix) или golang.org/x/sys/windows. Приведенный код демонстрирует структуру программы, готовую к интеграции системных вызовов.


Парсер URL и проверка доступности ресурса

Инструмент анализирует компоненты URL (хост, путь, порт) и проверяет, доступен ли сервер по этому адресу.

Код примера

package main

import (
"fmt"
"net/url"
"time"
)

func checkURL(target string) error {
parsedURL, err := url.Parse(target)
if err != nil {
return fmt.Errorf("неверный формат URL: %w", err)
}

fmt.Printf("Анализ URL: %s\n", target)
fmt.Printf(" Схема: %s\n", parsedURL.Scheme)
fmt.Printf(" Хост: %s\n", parsedURL.Host)
fmt.Printf(" Путь: %s\n", parsedURL.Path)
fmt.Printf(" Порт: %s\n", parsedURL.Port())

timeout := 5 * time.Second
client := &http.Client{
Timeout: timeout,
}

resp, err := client.Head(target)
if err != nil {
return fmt.Errorf("проверка недоступна: %w", err)
}
defer resp.Body.Close()

fmt.Printf(" Статус: %d\n", resp.StatusCode)
fmt.Printf(" Время ответа: OK\n")
return nil
}

func main() {
urls := []string{
"https://github.com",
"http://invalid-domain-xyz.com",
}

for _, u := range urls {
fmt.Println("---")
if err := checkURL(u); err != nil {
fmt.Println("Ошибка:", err)
}
}
}

Разбор кода

  • url.Parse: Разбивает строку URL на составные части (схема, хост, порт, путь).
  • parsedURL.Port(): Извлекает номер порта, если он явно указан.
  • client.Head: Отправка запроса метода HEAD, который получает только заголовки без тела ответа, что экономит трафик.
  • Timeout: Ограничение времени ожидания ответа для предотвращения зависания программы при недоступном сервере.

Конвертер форматов дат

Утилита конвертирует строковые представления дат в другие форматы, используя встроенный механизм форматирования времени.

Код примера

package main

import (
"fmt"
"time"
)

func convertDate(dateString, layout, outputLayout string) error {
loc, err := time.LoadLocation("UTC")
if err != nil {
return err
}

t, err := time.ParseInLocation(layout, dateString, loc)
if err != nil {
return fmt.Errorf("неверный формат входной даты: %w", err)
}

formatted := t.Format(outputLayout)
fmt.Printf("Вход: %s (%s)\n", dateString, layout)
fmt.Printf("Выход: %s (%s)\n", formatted, outputLayout)
return nil
}

func main() {
input := "2026-05-06"
layout := "2006-01-02"
output := "02 January 2006"

convertDate(input, layout, output)
}

Разбор кода

  • time.ParseInLocation: Парсинг строки в объект time.Time с учетом временной зоны.
  • t.Format: Преобразование объекта времени в строку по заданному шаблону.
  • Шаблон 2006-01-02: В Go используется специальное эталонное время Mon Jan 2 15:04:05 MST 2006 для определения формата. Любая дата в коде должна соответствовать этому шаблону.

Утилита для просмотра запущенных процессов

Отображение списка активных процессов с их идентификаторами (PID) и командами запуска.

Код примера

package main

import (
"fmt"
"os/exec"
"strings"
)

func listProcesses() error {
var cmd *exec.Cmd
var args []string

// Определение команды в зависимости от ОС
if isWindows() {
cmd = exec.Command("tasklist", "/FO", "CSV")
args = []string{}
} else {
cmd = exec.Command("ps", "aux")
args = []string{}
}

output, err := cmd.Output()
if err != nil {
return fmt.Errorf("ошибка выполнения команды: %w", err)
}

lines := strings.Split(string(output), "\n")
for i, line := range lines {
if i == 0 && !isWindows() {
continue // Пропуск заголовка в Linux/Mac
}
if line == "" {
continue
}
fmt.Println(line)
}
return nil
}

func isWindows() bool {
return true // Заглушка для примера, в реальности используйте runtime.GOOS
}

func main() {
fmt.Println("Список процессов:")
if err := listProcesses(); err != nil {
fmt.Println("Ошибка:", err)
}
}

Примечание: Для реальной работы с процессами в Go рекомендуется использовать пакеты вроде github.com/shirou/gopsutil, так как прямое выполнение команд (exec.Command) зависит от ОС и требует наличия специфических утилит. Данный пример демонстрирует принцип взаимодействия с оболочкой.


Характерный пример именно для Go: Горизонтальная масштабируемость через Горутины

Главная особенность языка Go — это нативная поддержка параллелизма через горутины (goroutines) и каналы (channels). Ниже приведен пример, который обрабатывает список URL параллельно, используя ограниченное количество горуток.

package main

import (
"fmt"
"net/http"
"sync"
"time"
)

type Result struct {
URL string
Status int
Error error
}

func checkConcurrency(urls []string, workers int) []Result {
results := make([]Result, len(urls))
var wg sync.WaitGroup
semaphore := make(chan struct{}, workers)

for i, url := range urls {
wg.Add(1)
go func(index int, target string) {
defer wg.Done()

semaphore <- struct{}{} // Занимаем слот
defer func() { <-semaphore }() // Освобождаем слот

client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Head(target)

status := 0
if err == nil {
status = resp.StatusCode
defer resp.Body.Close()
}

results[index] = Result{
URL: target,
Status: status,
Error: err,
}
}(i, url)
}

wg.Wait()
return results
}

func main() {
targets := []string{
"https://google.com",
"https://github.com",
"https://example.com",
"https://invalid-site-test.com",
}

fmt.Println("Запуск параллельной проверки...")
start := time.Now()
res := checkConcurrency(targets, 3) // Максимум 3 одновременных запроса
elapsed := time.Since(start)

for _, r := range res {
if r.Error != nil {
fmt.Printf("%s: Ошибка - %v\n", r.URL, r.Error)
} else {
fmt.Printf("%s: Статус %d\n", r.URL, r.Status)
}
}
fmt.Printf("Время выполнения: %v\n", elapsed)
}

Разбор кода

  • go func(...): Запуск функции в отдельной горуток. Горутина потребляет минимум памяти и переключается очень быстро.
  • sync.WaitGroup: Синхронизатор, позволяющий основной программе ждать завершения всех запущенных горуток.
  • chan struct{} (Семафор): Канал пустого типа, ограничивающий максимальное количество одновременно работающих горуток. Это защищает систему от перегрузки.
  • defer: Гарантирует освобождение ресурса канала даже при возникновении ошибки внутри горутины.
  • time.Since: Подсчет времени выполнения блока кода для оценки производительности.

Этот пример иллюстрирует философию Go: простота написания параллельного кода, безопасность потоков и высокая эффективность использования ресурсов процессора.


Освоение главы0%